| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 |
2x
2x
2x
2x
25151x
2x
43878x
39304x
4574x
43878x
39306x
4572x
43878x
43878x
43878x
2x
18727x
18727x
18727x
2x
1602222x
2x
11626x
2x
14155x
14155x
3987x
7322x
10168x
10168x
14155x
2x
160372x
2x
3022x
3022x
3022x
2x
1550x
1550x
2x
4449x
4449x
2x
1634x
1634x
2x
697629x
697629x
2x
110183x
2x
4561x
9x
4552x
4720x
15x
4537x
2x
100421x
103927x
2x
44162x
2x
165206x
165206x
323463x
323463x
323463x
287859x
51552x
51509x
50996x
2x
2x
2x
27641x
2x
16465x
2x
4418x
24x
23064x
4394x
2x
2x
2x
2x
2x
4025x
2x
3818x
4025x
4025x
87x
4025x
2x
160x
2x
13490x
2x
6x
2x
603x
603x
603x
603x
663x
4x
659x
659x
603x
603x
4215x
4215x
12x
1x
11x
11x
1x
10x
10x
4203x
44x
44x
4159x
64x
62x
4095x
4095x
599x
597x
597x
2x
2x
| /**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert, fail } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
export const DOCUMENT_KEY_NAME = '__name__';
/**
* Path represents an ordered sequence of string segments.
*/
export abstract class Path {
private segments: string[];
private offset: number;
private len: number;
constructor(segments: string[], offset?: number, length?: number) {
this.init(segments, offset, length);
}
/**
* An initialization method that can be called from outside the constructor.
* We need this so that we can have a non-static construct method that returns
* the polymorphic `this` type.
*/
private init(segments: string[], offset?: number, length?: number) {
if (offset === undefined) {
offset = 0;
} else Iif (offset > segments.length) {
fail('offset ' + offset + ' out of range ' + segments.length);
}
if (length === undefined) {
length = segments.length - offset;
} else Iif (length > segments.length - offset) {
fail('length ' + length + ' out of range ' + (segments.length - offset));
}
this.segments = segments;
this.offset = offset;
this.len = length;
}
/**
* Constructs a new instance of Path using the same concrete type as `this`.
* We need this instead of using the normal constructor, because polymorphic
* `this` doesn't work on static methods.
*/
private construct(
segments: string[],
offset?: number,
length?: number
): this {
const path: this = Object.create(Object.getPrototypeOf(this));
path.init(segments, offset, length);
return path;
}
get length(): number {
return this.len;
}
isEqual(other: Path): boolean {
return Path.comparator(this, other) === 0;
}
child(nameOrPath: string | this): this {
const segments = this.segments.slice(this.offset, this.limit());
if (nameOrPath instanceof Path) {
nameOrPath.forEach(segment => {
segments.push(segment);
});
} else Eif (typeof nameOrPath === 'string') {
segments.push(nameOrPath);
} else {
fail('Unknown parameter type for Path.child(): ' + nameOrPath);
}
return this.construct(segments);
}
/** The index of one past the last segment of the path. */
private limit(): number {
return this.offset + this.length;
}
popFirst(size?: number): this {
size = size === undefined ? 1 : size;
assert(this.length >= size, "Can't call popFirst() with less segments");
return this.construct(
this.segments,
this.offset + size,
this.length - size
);
}
popLast(): this {
assert(!this.isEmpty(), "Can't call popLast() on empty path");
return this.construct(this.segments, this.offset, this.length - 1);
}
firstSegment(): string {
assert(!this.isEmpty(), "Can't call firstSegment() on empty path");
return this.segments[this.offset];
}
lastSegment(): string {
assert(!this.isEmpty(), "Can't call lastSegment() on empty path");
return this.segments[this.limit() - 1];
}
get(index: number): string {
assert(index < this.length, 'Index out of range');
return this.segments[this.offset + index];
}
isEmpty(): boolean {
return this.length === 0;
}
isPrefixOf(other: this): boolean {
if (other.length < this.length) {
return false;
}
for (let i = 0; i < this.length; i++) {
if (this.get(i) !== other.get(i)) {
return false;
}
}
return true;
}
forEach(fn: (segment: string) => void): void {
for (let i = this.offset, end = this.limit(); i < end; i++) {
fn(this.segments[i]);
}
}
toArray(): string[] {
return this.segments.slice(this.offset, this.limit());
}
static comparator(p1: Path, p2: Path): number {
const len = Math.min(p1.length, p2.length);
for (let i = 0; i < len; i++) {
const left = p1.get(i);
const right = p2.get(i);
if (left < right) return -1;
if (left > right) return 1;
}
if (p1.length < p2.length) return -1;
if (p1.length > p2.length) return 1;
return 0;
}
}
/**
* A slash-separated path for navigating resources (documents and collections)
* within Firestore.
*/
export class ResourcePath extends Path {
canonicalString(): string {
// NOTE: The client is ignorant of any path segments containing escape
// sequences (e.g. __id123__) and just passes them through raw (they exist
// for legacy reasons and should not be used frequently).
return this.toArray().join('/');
}
toString(): string {
return this.canonicalString();
}
/**
* Creates a resource path from the given slash-delimited string.
*/
static fromString(path: string): ResourcePath {
// NOTE: The client is ignorant of any path segments containing escape
// sequences (e.g. __id123__) and just passes them through raw (they exist
// for legacy reasons and should not be used frequently).
if (path.indexOf('//') >= 0) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`Invalid path (${path}). Paths must not contain // in them.`
);
}
// We may still have an empty segment at the beginning or end if they had a
// leading or trailing slash (which we allow).
const segments = path.split('/').filter(segment => segment.length > 0);
return new ResourcePath(segments);
}
static EMPTY_PATH = new ResourcePath([]);
}
const identifierRegExp = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
/** A dot-separated path for navigating sub-objects within a document. */
export class FieldPath extends Path {
/**
* Returns true if the string could be used as a segment in a field path
* without escaping.
*/
private static isValidIdentifier(segment: string) {
return identifierRegExp.test(segment);
}
canonicalString(): string {
return this.toArray()
.map(str => {
str = str.replace('\\', '\\\\').replace('`', '\\`');
if (!FieldPath.isValidIdentifier(str)) {
str = '`' + str + '`';
}
return str;
})
.join('.');
}
toString(): string {
return this.canonicalString();
}
/**
* Returns true if this field references the key of a document.
*/
isKeyField(): boolean {
return this.length === 1 && this.get(0) === DOCUMENT_KEY_NAME;
}
/**
* The field designating the key of a document.
*/
static keyField(): FieldPath {
return new FieldPath([DOCUMENT_KEY_NAME]);
}
/**
* Parses a field string from the given server-formatted string.
*
* - Splitting the empty string is not allowed (for now at least).
* - Empty segments within the string (e.g. if there are two consecutive
* separators) are not allowed.
*
* TODO(b/37244157): we should make this more strict. Right now, it allows
* non-identifier path components, even if they aren't escaped.
*/
static fromServerFormat(path: string): FieldPath {
const segments: string[] = [];
let current = '';
let i = 0;
const addCurrentSegment = () => {
if (current.length === 0) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`Invalid field path (${path}). Paths must not be empty, begin ` +
`with '.', end with '.', or contain '..'`
);
}
segments.push(current);
current = '';
};
let inBackticks = false;
while (i < path.length) {
const c = path[i];
if (c === '\\') {
if (i + 1 === path.length) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Path has trailing escape character: ' + path
);
}
const next = path[i + 1];
if (!(next === '\\' || next === '.' || next === '`')) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Path has invalid escape sequence: ' + path
);
}
current += next;
i += 2;
} else if (c === '`') {
inBackticks = !inBackticks;
i++;
} else if (c === '.' && !inBackticks) {
addCurrentSegment();
i++;
} else {
current += c;
i++;
}
}
addCurrentSegment();
Iif (inBackticks) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Unterminated ` in path: ' + path
);
}
return new FieldPath(segments);
}
static EMPTY_PATH = new FieldPath([]);
}
|